#!/usr/bin/perl -w
use strict;
use warnings;
use File::Basename;
use File::stat;
use Cwd 'abs_path';

my $help_mode = 0;
my $help_detail_mode = 0;
my $verbose = 0;
my $list_mode = 0;
my $test_mode = 0;
my $enable_duplicate = 0;
my $sysinfo_mod = 0;

my %procedures;
my %global_variables = ( SEQPIPE => 'seqpipe', SEQPIPE_ROOT => (dirname abs_path $0) );
my $run_counter = 0;

my $seqpipe_log_dir = "./.seqpipe";
my $default_dir_attr = 0755;

my $return_value = 0;
my @module_files = ( (dirname abs_path $0) . "/default.pipe" );
my $command_line = "$0" . ($#ARGV >= 0 ? " " : "") . join(" ", @ARGV);

sub check_command_syntax
{
	my ($command) = @_;

	if ($command =~ /^SP_/) {
		return if $command =~ /^SP_run\s+(\w+)(\s+(\w+)="(.*?)")*\s*$/;
		return if $command =~ /^SP_set\s+(\w+)="(.*?)"(|(\s+(\w+)(\s+(\w+)="(.*?)")*))\s*$/;
		return if $command =~ /^SP_parallel_begin$/;
		return if $command =~ /^SP_parallel_end$/;
		die "Error: Invalid primitive line! $command";
	}
}

sub append_command
{
	my ($commands_ref, $defined_variables_ref, $used_variables_ref, $command) = @_;

	check_command_syntax $command;
	push(@{$commands_ref}, $command);

	if ($command =~ /^SP_set\s+(\w+)="(.*?)"/) {
		${$defined_variables_ref}{$1} = $2;
	}

	while ($command =~ /\${(\w+)}/g) {
		my $command_name = $1;
		if (!exists ${$used_variables_ref}{$command_name}) {
			${$used_variables_ref}{$command_name} = '';
		}
	}
}

sub load_module
{
	my ($module_file) = @_;
	my $line;

	open MODULE_FILE, $module_file or die "Can not open module file '$module_file'!";
	print "Load module file '$module_file'\n" if ($verbose);
	while ($line = <MODULE_FILE>) {
		chomp $line;

		if ($line =~ /^#\[procedure\s+type="(.*?)"\s*\]$/) {
			my $type = $1;
			my @inputs = ();
			my @outputs = ();
			my @requires = ();
			my $skip_record;
			my $ignore_retval;

			if ($type eq 'sysinfo' or $type eq 'checker') {
				$skip_record = 'Y';
				$ignore_retval = 'Y';
			} elsif ($type eq 'stage' or $type eq 'pipeline') {
				$skip_record = 'N';
				$ignore_retval = 'N';
			} else {
				die "Warning: Unknown type of procedure: '$type'!";
			}

			while ($line = <MODULE_FILE>) {
				if ($line =~ /^#\[workflow\s+(input|output|require|skip_record|ignore_retval)=.*\]$/) {
					while ($line =~ /\s+(input|output|require|skip_record|ignore_retval)="(.*?)"/g) {
						if ($1 eq "input") {
							push @inputs, $2;
						} elsif ($1 eq "output") {
							push @outputs, $2;
						} elsif ($1 eq "require") {
							push @requires, $2;
						} elsif ($1 eq "skip_record" or $1 eq "ignore_retval") {
							if ($2 ne "Y" and $2 ne "N") {
								die "Invalid value of '$1' attribute! Available values are: 'Y', 'N'";
							}
							if ($1 eq "skip_record") {
								$skip_record = $2;
							} else {
								$ignore_retval = $2;
							}
						}
					}
				} elsif ($line =~ /^\s*function\s+/) {
					die "Invalid procedure declaration!" if $line !~ /^function\s+(\w+)\s*$/;

					my $procedure_name = $1;
					my @commands = ();
					my %defined_variables = ();
					my %used_variables = ();

					if (exists $procedures{$procedure_name}) {
						print "Warning: Reassignment of global variable '$procedure_name'\n"
							if $verbose or !$enable_duplicate;
						die "You may use '-d' option to enable the duplicated procedure declaration."
							unless $enable_duplicate;
					}

					$line = <MODULE_FILE>;
					chomp $line;
					if ($line ne "{") {
						die "Invalid procedure declaration! Line '{' expected!";
					}
					my $last_line = '';
					while ($line = <MODULE_FILE>) {
						chomp $line;
						if ($line eq "}") {
							if ($last_line ne "") {
								die "Invalid procedure declaration! Last command line not finished!";
							}
							last;
						}

						if ($last_line eq '') {
							$line =~ s/^\s+//g;
						} else {
							my $b1 = $last_line =~ s/\s+$//g;
							my $b2 = $line =~ s/^\s+//g;
							$line = $last_line . (($b1 or $b2) ? ' ' : '') . $line;
							$last_line = '';
						}

						if ($line =~ s/\\$//) {
							$last_line = $line;
						} elsif ($line !~ /^\s*$/) {
							append_command \@commands, \%defined_variables, \%used_variables, $line;
						}
					}
					if ($line ne "}") {
						die "Invalid procedure declaration! Line '}' expected!";
					}

					foreach my $variable (@inputs, @outputs, @requires) {
						while ($variable =~ /\${(\w+)}/g) {
							my $variable_name = $1;
							if (!exists $used_variables{$variable_name}) {
								$used_variables{$variable_name} = '';
							}
						}
					}

					$procedures{$procedure_name} = {
						type              => $type,
						inputs            => \@inputs,
						outputs           => \@outputs,
						requires          => \@requires,
						commands          => \@commands,
						defined_variables => \%defined_variables,
						used_variables    => \%used_variables,
						skip_record       => $skip_record,
						ignore_retval     => $ignore_retval
					};
					last;
				}
			}
		} elsif ($line =~ /^\s*function\s+(\w+)\s*$/) {
			while ($line = <MODULE_FILE>) {
				chomp $line;
				last if $line eq "}";
			}
		} elsif ($line =~ /\s*(\w+)=(.*)$/) {
			my $option_name = $1;
			my $option_value = $2;
			$option_value =~ s/\s*#.*$//g;  # Remove tailing comments and whitespaces.
			if (exists $global_variables{$option_name}) {
				print "Warning: Reassignment of global variable '$option_name'\n"
					if $verbose or !$enable_duplicate;
				die "You may use '-d' option to enable the duplicated global variable declaration."
					unless $enable_duplicate;
			}
			while ($option_value =~ /\${(\w+)}/g) {
				if (exists $global_variables{$option_name}) {
					$option_value =~ s/\${$option_name}/$global_variables{$option_name}/g;
				} else {
					$option_value =~ s/\${$option_name}//g;
				}
			}
			$global_variables{$option_name} = $option_value;
		}
	}
	close MODULE_FILE;
}

sub print_usage
{
	print '
SeqPipe: a SEQuencing data analsysis PIPEline framework
Version: 0.1.0 ($Rev: 44 $)
Copyright: 2012, Centre for Bioinformatics, Peking University, China

Usage: ' . basename($0) . ' [options] <procedure> [NAME=VALUE ...]

Options:
   -h           Show this help.
   -H           Show detail help (including optional parameters).
   -v           Show verbose message.
   -m <file>    Load procedure module file, this option can be used many times.
   -d           Enable duplcated global variables and procedures (overwrited as the latters).
   -l           List current available procedures.
   -t           Test mode, only print the commands rather than execute them.
   -s           Show system info (by calling all sysinfo procedures).
';
}

sub list_procedures
{
	foreach my $type ("stage", "pipeline") {
		print "\nCurrent available procedures ($type):\n";
		foreach my $name (sort keys %procedures) {
			if ($procedures{$name}{type} eq $type) {
				print "   $name\n";
			}
		}
	}
	print "\n";
}

sub check_option
{
	my ($procedure_name, $variable_name) = @_;

	if (exists ${$procedures{$procedure_name}{variables}}{$variable_name}) {
		return 1;
	}
	return 0;
}

sub parse_line {
	my ($line) = @_;
	my $i = 0;
	my @parts = ();
	my $word = '';
	my $quot = '';

	while ($line ne '') {
		my $c = substr($line, 0, 1);
		$line = substr($line, 1);
		if ($c =~ /\s/) {
			push @parts, $word if ($word ne '');
			$word = '';
		} elsif ($c =~ /[\'\"]/) {
			$quot = $c;
			$word .= $c;
			while ($line ne '') {
				my $c = substr($line, 0, 1);
				$line = substr($line, 1);
				$word .= $c;
				if ($c eq '\\') {
					$c = substr($line, 0, 1);
					$line = substr($line, 1);
					$word .= $c;
					next;
				}
				if ($c eq $quot) { last; }
			}
		} else {
			$word .= $c;
		}
	}
	push @parts, $word if ($word ne '');
	return @parts;
}

sub run_procedure
{
	my ($procedure, $argv_ref, $is_test_mode) = @_;

	if (!exists $procedures{$procedure}) {
		print "Error: Unknown procedure '$procedure'! Use '-l' option to list available procedures.\n";
		exit 1;
	}
	my $record = ($procedures{$procedure}{skip_record} ne "Y");

	my %variables;
	foreach my $arg (keys %{$procedures{$procedure}{used_variables}}) {
		if (${$procedures{$procedure}{used_variables}}{$arg} ne "") {
			$variables{$arg} = ${$procedures{$procedure}{used_variables}}{$arg};
		}
	}
	foreach my $arg (keys %{$procedures{$procedure}{used_variables}}) {
		if (exists $global_variables{$arg} and $global_variables{$arg} ne "") {
			$variables{$arg} = $global_variables{$arg};
		}
	}
	foreach my $arg (keys %{$procedures{$procedure}{used_variables}}) {
		if (exists $ENV{$arg} and $ENV{$arg} ne "") {
			$variables{$arg} = $ENV{$arg}
		}
	}
	foreach my $arg (@{$argv_ref}) {
		if ($arg =~ /^(\w+)=(.*)$/) {
			my $option_name = $1;
			my $option_value = $2;
			if ($option_value =~ /^"(.*?)"$/) {
				$option_value = $1;
			}
			$variables{$option_name} = $option_value;
		} else {
			print "Error: Invalid procedure option '$arg'! It should be the format of 'NAME=VALUE'!\n";
			exit 1;
		}
	}
	foreach my $arg (%{$procedures{$procedure}{defined_variables}}) {
		if (!exists $variables{$arg}) {
			$variables{$arg} = "";
		}
	}
	foreach my $arg (sort keys %{$procedures{$procedure}{used_variables}}) {
		if (!exists $variables{$arg}) {
			print "Error: Option '$arg' is required for procedure '$procedure'!\n";
			exit 1;
		}
	}

	my $need_rerun = 0;
	my @inputs = @{$procedures{$procedure}{inputs}};
	my @outputs = @{$procedures{$procedure}{outputs}};
	my @requires = @{$procedures{$procedure}{requires}};
	my $i;
	for ($i = 0; $i < scalar @inputs; ++$i) {
		while ($inputs[$i] =~ /\${(\w+)}/) {
			my $option_name = $1;
			$inputs[$i] =~ s/\${$option_name}/$variables{$option_name}/g;
		}
		if (!-e $inputs[$i]) {
			print "Error: Input file '$inputs[$i]' of procedure '$procedure' does not exist!\n";
			exit 1;
		}
	}
	for ($i = 0; $i < scalar @requires; ++$i) {
		while ($requires[$i] =~ /\${(\w+)}/) {
			my $option_name = $1;
			$requires[$i] =~ s/\${$option_name}/$variables{$option_name}/g;
		}
		if (!-e $requires[$i]) {
			print "Error: Input file '$requires[$i]' of procedure '$procedure' does not exist!\n";
			exit 1;
		}
	}
	if (scalar @outputs == 0) {
		$need_rerun = 1;
	} else {
		for ($i = 0; $i < scalar @outputs; ++$i) {
			while ($outputs[$i] =~ /\${(\w+)}/) {
				my $option_name = $1;
				$outputs[$i] =~ s/\${$option_name}/$variables{$option_name}/g;
			}
			if (-e $outputs[$i]) {
				foreach my $file (@inputs) {
					if ((stat($file))->mtime > (stat($outputs[$i]))->mtime) {
						$need_rerun = 1;
						last;
					}
				}
			} else {
				$need_rerun = 1;
				last;
			}
			last if ($need_rerun);
		}
	}

	if ($is_test_mode) {
		foreach my $cmd (@{$procedures{$procedure}{commands}}) {
			my $command = $cmd;
			while ($command =~ /\${(\w+)}/) {
				my $option_name = $1;
				my $option_value = $variables{$option_name};
				$option_value = "" if (!defined($option_value));
				$command =~ s/\${$option_name}/$option_value/g;
			}

			if ($command =~ /^SP_set\s+(\w+)="(.*?)"(|(\s+(\w+)(\s+(\w+)="(.*?)")*))\s*$/) {
				if ($verbose) {
					print "#$command\n";
				}
				my @procedure_options = parse_line($command);
				shift @procedure_options;
				my $setting = shift @procedure_options;
				if (scalar @procedure_options == 0) {
					$setting =~ /(\w+)="(.*?)"/;
					$variables{$1} = $2;
				} else {
					my $procedure_name = shift @procedure_options;
					push @procedure_options, @{$argv_ref};
					run_procedure($procedure_name, \@procedure_options, 0);
					if ($return_value == 0) {
						$setting =~ /(\w+)="(.*?)"/;
						$variables{$1} = $2;
					}
				}
			} elsif ($command =~ /^SP_run\s+(\w+)/) {
				if ($verbose) {
					print "#$command\n";
				}
				if ($need_rerun) {
					my @procedure_options = parse_line($command);
					shift @procedure_options;
					my $procedure_name = shift @procedure_options;
					push @procedure_options, @{$argv_ref};
					run_procedure($procedure_name, \@procedure_options, $is_test_mode);
				}
			} elsif ($command == 'SP_parallel_begin' or $command == 'SP_parallel_end') {
				if ($verbose) {
					print "#$command\n";
				}
			} else {
				if ($verbose or $procedures{$procedure}{skip_record} ne "Y") {
					print "#" unless ($need_rerun);
					print "$command\n";
				}
			}
		}
		return 0;
	}

	return 0 unless $need_rerun;

	my $in_parallel = 0;
	my $parallel_begin = 0;
	my @command_set = ();
	my @command_with_log_set = ();
	my $first_cmd_in_procedure = 1;
	foreach my $cmd (@{$procedures{$procedure}{commands}}) {
		my $command = $cmd;
		while ($command =~ /\${(\w+)}/) {
			my $option_name = $1;
			$command =~ s/\${$option_name}/$variables{$option_name}/g;
		}

		if ($command =~ /^SP_set\s+(\w+)="(.*?)"(|(\s+(\w+)(\s+(\w+)="(.*?)")*))\s*$/) {
			my @procedure_options = parse_line($command);
			shift @procedure_options;
			my $setting = shift @procedure_options;
			if (scalar @procedure_options == 0) {
				$setting =~ /(\w+)="(.*?)"/;
				$variables{$1} = $2;
			} else {
				my $procedure_name = shift @procedure_options;
				push @procedure_options, @{$argv_ref};
				run_procedure($procedure_name, \@procedure_options, 0);
				if ($return_value == 0) {
					$setting =~ /(\w+)="(.*?)"/;
					$variables{$1} = $2;
					print LOG_FILE "[SeqPipe] Option '$1' changes to '$2'.\n";
				}
			}
			next;
		}
		if ($command =~ /^SP_run\s+(\w+)/) {
			my @procedure_options = parse_line($command);
			shift @procedure_options;
			my $procedure_name = shift @procedure_options;
			push @procedure_options, @{$argv_ref};
			
			if ($record) {
				print LOG_FILE "[SeqPipe] Run sub-pipeline(stage): $procedure_name\n";
			}
			run_procedure($procedure_name, \@procedure_options, $is_test_mode);
			next;
		}

		if ($command eq 'SP_parallel_begin') {
			die "Already in parallel mode, duplicated '$command'!" if $in_parallel;
			$parallel_begin = $run_counter + 1;
			$in_parallel = 1;
			next;
		}
		if ($command eq 'SP_parallel_end') {
			die "Not in parallel mode, unexpected '$command'!" unless $in_parallel;
		}

		my $record_id = '';
		my $command_with_log = '';
		if ($command ne 'SP_parallel_end') {
			if ($procedures{$procedure}{type} eq 'sysinfo') {
				$command_with_log = "($command) 2>&1 >>$seqpipe_log_dir/$$.sysinfo";
			} elsif ($procedures{$procedure}{skip_record} eq "Y") {
				$command_with_log = "($command) >/dev/null 2>/dev/null";
			} else {
				my $hook_begin = ($first_cmd_in_procedure ? "" : ". $seqpipe_log_dir/$$.$run_counter.$procedure.env;");
				$run_counter += 1;
				$record_id = "$$.$run_counter";
				my $hook_end = "; env > $seqpipe_log_dir/$record_id.$procedure.env";
				$first_cmd_in_procedure = 0;

				$command_with_log = "($hook_begin $command $hook_end) >>$seqpipe_log_dir/$record_id.$procedure.log 2>>$seqpipe_log_dir/$record_id.$procedure.err";

				open FILE, ">>" . "$seqpipe_log_dir/$record_id.$procedure.cmd";
				printf FILE "$command\n";
				close FILE;
			}
		}
		
		my $launcher = "/bin/bash";
		if ($in_parallel) {
			if ($command ne 'SP_parallel_end') {
				push @command_set, $command;
				push @command_with_log_set, $command_with_log;
				next;
			} else {
				$launcher = "parallel";
				$in_parallel = 0;
				$command_with_log = join("\n", @command_with_log_set);
				if ($parallel_begin != $run_counter) {
					$record_id = "$$.$parallel_begin-$run_counter";
				}
			}
		}
	
		my ($start_time, $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst);
		if ($record) {
			if ($launcher eq 'parallel') {
				foreach my $each_cmd (@command_set) {
					print LOG_FILE "[SeqPipe] Command($record_id): $each_cmd\n";
				}
			} else {
				print LOG_FILE "[SeqPipe] Command($record_id): $command\n";
			}

			$start_time = time;
			($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime($start_time);
			printf LOG_FILE "[SeqPipe] Command($record_id) starts at %04d-%02d-%02d %02d:%02d:%02d\n",
				$year + 1900, $mon + 1, $mday, $hour, $min, $sec;
		}
		
		open BASH, "| $launcher" or die "Can not launch command(s): $command";
		print BASH $command_with_log;
		close BASH;
		
		if ($? == -1) {
			print LOG_FILE "[SeqPipe] Command($record_id) starts failed!\n";
			close LOG_FILE;
			exit 1;
		} elsif ($? & 127) {
			print LOG_FILE "[SeqPipe] Command($record_id) starts failed! Child died with signal %d, %s coredump\n",
				($? & 127), ($? & 128) ? 'with' : 'without';
			close LOG_FILE;
			exit 1;
		} else {
			$return_value = ($? >> 8);
		}

		if (($procedures{$procedure}{type} ne 'sysinfo') and
			($procedures{$procedure}{skip_record} eq 'N')) {
			my $last_line = '';
			open ENV_FILE, "$seqpipe_log_dir/$record_id.$procedure.env";
			print "ENV file: $seqpipe_log_dir/$record_id.$procedure.env\n";
			while (my $line = <ENV_FILE>) {
				chomp $line;
				if ($last_line eq '') {
					$line =~ s/^\s+//g;
				} else {
					my $b1 = ($last_line =~ s/\s+$//g);
					my $b2 = ($line =~ s/^\s+//g);
					$line = $last_line . (($b1 or $b2) ? ' ' : '') . $line;
					$last_line = '';
				}
				if ($line =~ s/\\$//g) {
					$last_line = $line;
				} elsif ($line =~ /^(\w+)=(.*)$/) {
					$variables{$1} = $2;
				}
			}
			close ENV_FILE;
		}

		if ($record) {
			my $end_time = time;
			my $elapsed_time = $end_time - $start_time;
			my $time_text = "";
			if ($elapsed_time >= 86400) {
				$time_text .= int($elapsed_time / 86400) . "d ";
				$elapsed_time %= 86400;
			}
			if ($time_text ne "" or $elapsed_time >= 3600) {
				$time_text .= int($elapsed_time / 3600) . "h ";
				$elapsed_time %= 3600;
			}
			if ($time_text ne "" or $elapsed_time >= 60) {
				$time_text .= int($elapsed_time / 60) . "m ";
				$elapsed_time %= 60;
			}
			if ($time_text eq "" or $elapsed_time > 0) {
				$time_text .= $elapsed_time . "s ";
			}
			$time_text =~ s/\s$//g;
			($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime($end_time);
			printf LOG_FILE "[SeqPipe] Command($record_id) ends at %04d-%02d-%02d %02d:%02d:%02d (elapsed: %s)\n",
				$year + 1900, $mon + 1, $mday, $hour, $min, $sec, $time_text;
		}
		
		if ($procedures{$procedure}{ignore_retval} eq 'N') {
			if ($return_value != 0) {
				print LOG_FILE "[SeqPipe] Command($$:$run_counter) returns $return_value\n";
				exit $return_value;
			}
		}
	}
	return $return_value;
}

my $procedure = "";

if ($#ARGV < 0) {
	$help_mode = 1;
} else {
	while (my $arg = shift @ARGV) {
		if ($arg eq '-h') {
			$help_mode = 1;
		} elsif ($arg eq '-H') {
			$help_mode = 1;
			$help_detail_mode = 1;
		} elsif ($arg eq '-v') {
			$verbose = 1;
		} elsif ($arg eq '-d') {
			$enable_duplicate = 1;
		} elsif ($arg eq '-l') {
			$list_mode = 1;
		} elsif ($arg eq '-t') {
			$test_mode = 1;
		} elsif ($arg eq '-m') {
			if ($#ARGV < 0) {
				print "Error: missing argument for option '$arg'!\n";
				exit 1;
			}
			push(@module_files, shift @ARGV);
		} elsif ($arg eq '-s') {
			$sysinfo_mod = 1;
		} elsif ($arg =~ '^-') {
			print "Error: Unknown option '$arg'!\n";
			exit 1;
		} else {
			$procedure = $arg;
			last;
		}
	}
}
foreach my $arg (@ARGV) {
	if ($arg eq "-h") {
		$help_mode = 1;
		last;
	} elsif ($arg eq "-H") {
		$help_mode = 1;
		$help_detail_mode = 1;
		last;
	}
}
if ($procedure eq "" and !$sysinfo_mod) {
	$help_mode = 1;
}

foreach my $module_file (@module_files) {
	load_module $module_file;
}
if ($verbose) {
	printf "All %d module file(s) loaded, including %d procedure(s).\n",
		scalar @module_files, scalar keys %procedures;
}

if ($list_mode) {
	list_procedures;
	exit 1;
}
if ($help_mode and $procedure eq "") {
	print_usage;
	print "\n";
	exit 1;
}

if ($help_mode) {
	if (!exists $procedures{$procedure}) {
		print "Error: Unknown procedure '$procedure'! Use '-l' option to list available procedures.\n";
		exit 1;
	}

	print_usage;

	print "
Options for procedure '$procedure':
";
	my $used_var_ref = \%{$procedures{$procedure}{used_variables}};
	my $defined_var_ref = \%{$procedures{$procedure}{defined_variables}};
	for my $required (1,0) {
		foreach my $variable_name (sort keys %{$used_var_ref}) {
			my $default_value = ${$used_var_ref}{$variable_name};
			if (exists $global_variables{$variable_name} and $global_variables{$variable_name} ne "") {
				$default_value = $global_variables{$variable_name};
			}
			my $is_required = ($default_value eq "" and !exists ${$defined_var_ref}{$variable_name});
			if ($required == $is_required) {
				printf "   %-30s %s\n", $variable_name, ($is_required ? "Required" : "Def: " . $default_value);
			}
		}
		if (!$help_detail_mode) { last; }
	}
	exit 1;
}

mkdir $seqpipe_log_dir, $default_dir_attr unless (-d $seqpipe_log_dir);

open LOG_FILE, ">>" . $seqpipe_log_dir . "/history.log";
printf LOG_FILE "%d\t%s\n", $$, $command_line;
close LOG_FILE;

my $start_time = time;
if (!$test_mode) {
	open LOG_FILE, "| tee -ai " . $seqpipe_log_dir . "/$$.log";

	printf LOG_FILE "[SeqPipe] Pipeline($$): $command_line\n";

	my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime($start_time);
	printf LOG_FILE "[SeqPipe] Pipeline($$) starts at %04d-%02d-%02d %02d:%02d:%02d\n",
		$year + 1900, $mon + 1, $mday, $hour, $min, $sec;

	foreach $procedure (keys %procedures) {
		if ($procedures{$procedure}{type} eq 'sysinfo') {
			run_procedure $procedure;
		}
	}
}

if (!$sysinfo_mod) {
	run_procedure $procedure, \@ARGV, $test_mode;
}

if (!$test_mode) {
	my $end_time = time;
	my $elapsed_time = $end_time - $start_time;
	my $time_text = "";
	if ($elapsed_time >= 86400) {
		$time_text .= int($elapsed_time / 86400) . "d ";
		$elapsed_time %= 86400;
	}
	if ($time_text ne "" or $elapsed_time >= 3600) {
		$time_text .= int($elapsed_time / 3600) . "h ";
		$elapsed_time %= 3600;
	}
	if ($time_text ne "" or $elapsed_time >= 60) {
		$time_text .= int($elapsed_time / 60) . "m ";
		$elapsed_time %= 60;
	}
	if ($time_text eq "" or $elapsed_time > 0) {
		$time_text .= $elapsed_time . "s ";
	}
	$time_text =~ s/\s$//g;
	my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime($end_time);
	printf LOG_FILE "[SeqPipe] Pipeline($$) ends at %04d-%02d-%02d %02d:%02d:%02d (elapsed: %s)\n",
		$year + 1900, $mon + 1, $mday, $hour, $min, $sec, $time_text;

	close LOG_FILE;
}

if ($sysinfo_mod) {
	open FILE, "$seqpipe_log_dir/$$.sysinfo";
	while (<FILE>) { print; }
	close FILE;
}

exit $return_value;
